Sometimes, the default automatic serialization provided by the Serializable
attribute is insufficient. Perhaps the object state contains sensitive
information, such as a credit card number. In that case, you may want to
encrypt the state instead of using a plain by-value serialization. Some
other examples that might require custom
serialization solutions are if you have some internal knowledge of how
to serialize the event subscribers, if which members get serialized
depends on the state of the object, or if you want to perform additional
proprietary initialization steps during deserialization.
1. The ISerializable Interface
.NET provides an easy-to-use mechanism for custom
serialization that extends the serialization infrastructure. To provide
custom serialization and deserialization behavior, you need to implement the ISerializable interface, defined in the System.Runtime.Serialization namespace:
public interface ISerializable
{
void GetObjectData(SerializationInfo info,StreamingContext context);
}
Every time a client serializes an object, .NET reflects the object's metadata to see whether the serializable object implements ISerializable. If it does, .NET calls GetObjectData( )
to retrieve the object's state. At this point, it's up to the object to
provide the state information in whichever way it wants. You will see
an example of implementing
ISerializable shortly.
To support the matching custom deserialization, the object must provide a special parameterized custom deserialization constructor with this signature:
<Class Name>(SerializationInfo info,StreamingContext context);
.NET calls this constructor during deserialization. The constructor can (and should) be defined as protected
to prevent normal clients from calling it. .NET uses reflection to
invoke the custom deserialization constructor and thus isn't impeded by
the constructor being protected. If the class implements the ISerializable
interface but doesn't provide a custom deserialization constructor, the
compiler doesn't warn you. Instead, during deserialization, .NET throws
an exception of type SerializationException.
In .NET, interfaces aren't allowed to have any
implementation details and therefore can't define constructors. The
design decision the architects of .NET serialization took was to force a
runtime check for the custom deserialization constructor, instead of a
compile-time check. However, I believe a better design decision would
have been to provide a SetObjectData( ) method on ISerializable and, during deserialization, to use reflection to set the fields of a new object. |
|
Note that the client isn't required to treat an object that implements ISerializable
any differently from an object that uses automatic serialization.
Custom serialization is purely a component-side facility. The client
uses the formatters and streams, as with automatic serialization.
2. Implementing ISerializable
Both GetObjectData( ) and the custom deserialization constructor accept a parameter of type SerializationInfo called info. SerializationInfo provides methods for getting or adding field values. Each field is identified by a string. Because SerializationInfo was defined before generics were available, SerializationInfo has type-safe methods for most of the CLR-defined types, such as int and string. For each such type, SerializationInfo provides two methods in this form:
void AddValue(string name, <Type> value);
<Type> Get<Type>(string name);
Here's an example:
public sealed class SerializationInfo
{
public void AddValue(string name, short value);
public void AddValue(string name, int value);
//Other AddValue( ) methods
public int GetInt32(string name);
public string GetString(string name);
//Other Get<Type>( ) methods
//Other methods and properties
}
For all other field types, SerializationInfo provides methods like these to add or get an object:
public void AddValue(string name, object value);
public object GetValue(string name, Type type);
The second parameter that both GetObjectData( ) and the custom deserialization constructor accept is the context parameter, of type StreamingContext. Example 1 demonstrates both ISerializable and the custom deserialization constructor. The way the class in Example 9-15
implements custom serialization has no advantage over automatic
serialization; it simply shows how to provide custom serialization. It's
up to you to provide the required custom steps.
Example 1. Implementing ISerializable
[Serializable]
public class MyClass : ISerializable
{
int m_Number;
string m_SomeString;
public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("m_Number",m_Number);
info.AddValue("m_SomeString",m_SomeString);
}
protected MyClass(SerializationInfo info,StreamingContext context)
{
m_Number = info.GetInt32("m_Number");
m_SomeString = info.GetString("m_SomeString");
}
public MyClass( )
{}
}
|
When you implement ISerializable, the type must still be decorated with the Serializable attribute. Otherwise, .NET considers the type non-serializable and ignores ISerializable. |
|
If the serialized class is a generic class, you need to use the AddValue( ) and GetValue( ) methods that accept an object while specifying the type, as shown in Example 2.
Example 2. Custom serialization of a generic class
[Serializable]
public class MyClass<T> : ISerializable
{
T m_T;
public MyClass( )
{}
public void GetObjectData(SerializationInfo info,StreamingContext ctx)
{
info.AddValue("m_T",m_T,typeof(T));
}
protected MyClass(SerializationInfo info,StreamingContext context)
{
m_T = (T)info.GetValue("m_T",typeof(T));
}
}
|
2.1. GenericSerializationInfo
The introduction of generics in .NET 2.0 allows you to improve on the available SerializationInfo and shield the client code from the type retrieval and explicit casting.
Example 3 presents the GenericSerializationInfo utility class, which exposes generic AddValue( ) and GetValue( ) methods. GenericSerializationInfo encapsulates a regular SerializationInfo object, passed to it as a construction parameter.
Example 3. The GenericSerializationInfo utility class
public class GenericSerializationInfo
{
SerializationInfo m_SerializationInfo;
public GenericSerializationInfo(SerializationInfo info)
{
m_SerializationInfo = info;
}
public void AddValue<T>(string name,T value)
{
m_SerializationInfo.AddValue(name,value,value.GetType( ));
}
public T GetValue<T>(string name)
{
object obj = m_SerializationInfo.GetValue(name,typeof(T));
return (T)obj;
}
}
|
Example 4 shows the same custom serialization code as Example 9-16, except it uses GenericSerializationInfo. Note the use of type inference in the call to AddValue( ).
Example 4. Using GenericSerializationInfo
[Serializable]
public class MyClass<T> : ISerializable
{
T m_T;
public MyClass( )
{}
public void GetObjectData(SerializationInfo info,StreamingContext ctx)
{
GenericSerializationInfo genericInfo = new GenericSerializationInfo(info);
genericInfo.AddValue("m_T",m_T); //Using type inference
}
protected MyClass(SerializationInfo info,StreamingContext context)
{
GenericSerializationInfo genericInfo = new GenericSerializationInfo(info);
m_T = genericInfo.GetValue<T>("m_T");
}
}
|
GenericSerializationInfo is a cleaner way of using custom serialization, even on non-generic-type class members. For example, using GenericSerializationInfo in the class MyClass from Example 1, you could write:
genericInfo.AddValue("m_SomeString",m_SomeString);
and:
m_SomeString = genericInfo.GetValue<string>("m_SomeString");
2.2. Custom serialization and IDeserializationCallback
Implementing IDeserializationCallback
allows a type to be notified after deserialization takes place and to
perform additional, custom deserialization steps. You can implement both
IDeserializationCallback and ISerializable, but when you implement ISerializable there is really no need for IDeserializationCallback, because you can place the custom steps in the custom deserialization constructor.
2.3. Custom serialization and serialization events
It is technically possible to use the serializing and deserializing events for custom serialization and avoid implementing ISerializable;
however, this will come at a high price in programming model and code
readability and maintainability. You will need to have a separate set of
member variables, all marked as non-serializable. The type itself will
use those members, and the serialization events will perform the custom
steps (such as encryption or decryption) on the non-serializable members
and then copy them to the set of serializable member variables, whose
sole purpose will be to be serialized and deserialized. Needless to say,
such an approach is cumbersome and error-prone. It is better to stick
with the dedicated ISerializable standard mechanism.